/*
 * Copyright (c) 2009, Hamish Morgan. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the University of Sussex nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package piccat.model;

import java.util.Arrays;
import java.util.EventListener;
import java.util.EventObject;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.swing.event.EventListenerList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import piccat.lib.Grid;
import piccat.lib.Index;

/**
 * Class to hold information about each side of the game. Since the sides are
 * identical a Game object will hold two Side objects.
 *
 * @author Hamish Morgan
 */
public class Side {

    protected final Logger userLog = LoggerFactory.getLogger("piccat.userLog");
    /**
     * Set of all picture currently visible to the player
     */
    private final Set<Picture> pics;
    /**
     * Matrix of pictures that have been positioned in the grid
     */
    private final Grid<Picture> solution;
    /**
     * Whether or not the ready button is pressed
     */
    private boolean ready = false;
    private final EventListenerList listenerList =
            new EventListenerList();
    private ReadinessChangedEvent readynessChangedEvent = null;
    private int playerId;

    public Side(int rows, int cols) {
        super();
        solution = new Grid<Picture>(rows, cols);
        pics = new HashSet<Picture>();
        playerId = Picture.NO_OWNER;
    }

    public void setPlayerId(int playerId) {
        this.playerId = playerId;
    }

    public int getPlayerId() {
        return playerId;
    }

    public int getRows() {
        return solution.getRows();
    }

    public int getCols() {
        return solution.getCols();
    }

    public void pickup(Picture p) {
        final Index i = solution.indexOf(p);
        if (i != null) {
            solution.set(i, null);
            firePicturePickupEvent(p, i);
        }
    }

    public void drop(int row, int col, Picture p) {
        if (isCellEmpty(row, col)) {
            solution.set(row, col, p);
            firePictureDropEvent(p, row, col);
        }
    }

    public boolean isCellEmpty(int row, int col) {
        return solution.get(row, col) == null;
    }

    public void addNextPic(Picture pic) {
        Picture copy = pic.clone();
        copy.setOwnerId(playerId);
        pics.add(copy);
        firePictureAddedEvent(copy);
    }

    public Set<Picture> getPics() {
        return pics;
    }

    public boolean containsPic(Picture pic) {
        for (Picture p : pics)
            if (pic == p)
                return true;
        return false;
    }

    public boolean solutionContains(Picture p) {
        return solution.contains(p);
    }

    public Index solutionIndexOf(Picture p) {
        return solution.indexOf(p);
    }

    public boolean isReady() {
        return ready;
    }

    public void setReady(boolean ready) {
        if (ready != this.ready) {
            this.ready = ready;
            fireReadinessChangeEvent();
        }
    }

    public boolean isComplete() {
        int count = 0;
        for (Picture p : solution) {
            if (p != null)
                count++;
        }
        return count == pics.size();
    }

    public Set<Picture> getIncompletePictures() {
        final Set<Picture> result = new HashSet<Picture>();
        for (Picture pic : getPics()) {
            if (!solutionContains(pic)) {
                result.add(pic);
            }
        }
        return result;
    }

    public boolean isValid() {

        String[] cols = new String[solution.getCols()];
        Arrays.fill(cols, null);

        for (int j = 0; j < solution.getCols(); j++) {
            for (int i = 0; i < solution.getRows(); i++) {
                if (solution.get(i, j) == null)
                    continue;
                final Picture p = solution.get(i, j);
                if (cols[j] == null) {
                    cols[j] = p.getType();
                } else if (!p.getType().equals(cols[j]))
                    return false;
            }
        }
        for (int i = 0; i < cols.length - 1; i++) {
            for (int j = i + 1; j < cols.length; j++) {
                if (cols[i] != null && cols[j] != null && cols[i].equals(
                        cols[j]))
                    return false;
            }
        }

        return true;
    }

    public Set<Picture> getInvalidPictures() {

        final boolean[] colValid = new boolean[solution.getCols()];
        Arrays.fill(colValid, true);

        String[] cols = new String[solution.getCols()];
        Arrays.fill(cols, null);
        for (int c = 0; c < solution.getCols(); c++) {
//            String type = null;
            for (int r = 0; colValid[c] && r < solution.getRows(); r++) {
                final Picture p = solution.get(r, c);
                if (p != null) {
                    if (cols[c] == null)
                        cols[c] = p.getType();
                    else if (!cols[c].equals(p.getType()))
                        colValid[c] = false;
                }
            }
        }
        for (int i = 0; i < cols.length - 1; i++) {
            for (int j = i + 1; j < cols.length; j++) {
                if (cols[i] != null && cols[j] != null && cols[i].equals(
                        cols[j])) {
                    colValid[i] = false;
                    colValid[j] = false;
                }
            }
        }
        final Set<Picture> result = new HashSet<Picture>();
        for (int c = 0; c < solution.getCols(); c++) {
            if (colValid[c])
                continue;
            for (int r = 0; r < solution.getRows(); r++) {
                final Picture p = solution.get(r, c);
                if (p != null)
                    result.add(p);
            }
        }
        return result;
    }

    public boolean isFinished() {
        for (Picture p : solution) {
            if (p == null)
                return false;
        }
        return true;
    }
//

    public boolean isMatching(Side other) {
        final Iterator<Picture> thisIt = this.solution.iterator();
        final Iterator<Picture> otherIt = other.solution.iterator();

        while (thisIt.hasNext() && otherIt.hasNext()) {
            final Picture thisPic = thisIt.next();
            final Picture otherPic = otherIt.next();
            if (thisPic != otherPic && (thisPic == null || otherPic == null || !thisPic.matches(
                    otherPic)))
                return false;
        }
        return !(thisIt.hasNext() || otherIt.hasNext());
    }

    // Find all the pictures on this side that do not match one on the
    // other side
    public Set<Picture> getNonMatchingPictures(Side other) {
        final Set<Picture> result = new HashSet<Picture>();

        final Iterator<Picture> thisIt = this.solution.iterator();
        final Iterator<Picture> otherIt = other.solution.iterator();

        while (thisIt.hasNext() && otherIt.hasNext()) {
            final Picture thisPic = thisIt.next();
            final Picture otherPic = otherIt.next();

            if (thisPic == null) {
                continue;
            } else if (otherPic == null) {
                result.add(thisPic);
            } else if (!thisPic.matches(otherPic)) {
                result.add(thisPic);
            }
        }
        // If this grid is bigger (which it really shouldn't be) then add
        // the remainder.
        while (thisIt.hasNext())
            result.add(thisIt.next());

        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        final Side other = (Side) obj;
        if (this.solution != other.solution &&
                (this.solution == null ||
                !this.solution.equals(other.solution)))
            return false;
        if (this.pics != other.pics &&
                (this.pics == null ||
                !this.pics.equals(other.pics))) return false;
//        if (this.ready != other.ready) return false;
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 53 * hash +
                (this.solution != null ? this.solution.hashCode() : 0);
        hash =
                53 * hash +
                (this.pics != null ? this.pics.hashCode() : 0);
        return hash;
    }
    //
    // ==========================
    //

    public void addChangeListener(SideChangeListener listener) {
        listenerList.add(SideChangeListener.class, listener);
    }

    public void removeChangeListener(SideChangeListener listener) {
        listenerList.remove(SideChangeListener.class, listener);
    }

    public SideChangeListener[] getChangeListeners() {
        return listenerList.getListeners(SideChangeListener.class);
    }

    public int getChangeListenerCount() {
        return listenerList.getListenerCount(SideChangeListener.class);
    }

    protected void firePictureAddedEvent(Picture picture) {
        PictureAddedEvent pictureAddedEvent = null;
        final Object[] listeners = listenerList.getListenerList();
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == SideChangeListener.class) {
                if (pictureAddedEvent == null)
                    pictureAddedEvent = new PictureAddedEvent(this, picture);
                ((SideChangeListener) listeners[i + 1]).pictureAdded(
                        pictureAddedEvent);
            }
        }
    }

    protected void fireReadinessChangeEvent() {
        userLog.debug("Player #{} readyness has changed to {}",
                new Object[]{playerId, isReady() ? "READY" : "NOT READY"});
        final Object[] listeners = listenerList.getListenerList();
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == SideChangeListener.class) {
                if (readynessChangedEvent == null)
                    readynessChangedEvent = new ReadinessChangedEvent(this);
                ((SideChangeListener) listeners[i + 1]).readinessChanged(
                        readynessChangedEvent);
            }
        }
    }

    protected void firePicturePickupEvent(Picture picture, Index index) {
        userLog.debug("Player #{} has picked up picture {} from grid cell {}",
                new Object[]{playerId, picture, index});
        PicturePickupEvent pickturePickupEvent = null;
        final Object[] listeners = listenerList.getListenerList();
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == SideChangeListener.class) {
                if (pickturePickupEvent == null)
                    pickturePickupEvent =
                            new PicturePickupEvent(this, picture, index);
                ((SideChangeListener) listeners[i + 1]).picturePickedUp(
                        pickturePickupEvent);
            }
        }
    }

    protected void firePictureDropEvent(Picture picture, Index index) {
        userLog.debug("Player #{} has dropped picture {} into grid cell {}",
                new Object[]{playerId, picture, index});
        PictureDroppedEvent pictureDropEvent = null;
        final Object[] listeners = listenerList.getListenerList();
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == SideChangeListener.class) {
                if (pictureDropEvent == null)
                    pictureDropEvent = new PictureDroppedEvent(this, picture,
                            index);
                ((SideChangeListener) listeners[i + 1]).pictureDropped(
                        pictureDropEvent);
            }
        }
    }

    protected void firePictureDropEvent(Picture picture, int row, int col) {
        userLog.debug(
                "Player #{} has dropped picture {} into grid cell ({},{})",
                new Object[]{playerId, picture, row, col});
        PictureDroppedEvent pictureDropEvent = null;
        final Object[] listeners = listenerList.getListenerList();
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == SideChangeListener.class) {
                if (pictureDropEvent == null)
                    pictureDropEvent = new PictureDroppedEvent(this, picture,
                            row, col);
                ((SideChangeListener) listeners[i + 1]).pictureDropped(
                        pictureDropEvent);
            }
        }
    }

    public static interface SideChangeListener extends EventListener {

        void pictureAdded(PictureAddedEvent e);

        void readinessChanged(ReadinessChangedEvent e);

        void picturePickedUp(PicturePickupEvent e);

        void pictureDropped(PictureDroppedEvent e);
    }

    public static class ReadinessChangedEvent extends EventObject {

        public ReadinessChangedEvent(Side source) {
            super(source);
        }

        @Override
        public Side getSource() {
            return (Side) super.getSource();
        }
    }

    public static class PictureAddedEvent extends ReadinessChangedEvent {

        private final Picture picture;

        public PictureAddedEvent(Side source, Picture picture) {
            super(source);
            this.picture = picture;
        }

        public Picture getPicture() {
            return picture;
        }
    }

    public static class PictureDroppedEvent extends PictureAddedEvent {

        private final Index index;

        public PictureDroppedEvent(Side source, Picture picture, Index index) {
            super(source, picture);
            this.index = index;
        }

        public PictureDroppedEvent(Side source, Picture picture, int row,
                int col) {
            this(source, picture, new Index(row, col));
        }

        public Index getIndex() {
            return index;
        }

        public int getRow() {
            return index.getRow();
        }

        public int getCol() {
            return index.getCol();
        }
    }

    public static class PicturePickupEvent extends PictureDroppedEvent {

        public PicturePickupEvent(Side source, Picture picture, Index index) {
            super(source, picture, index);
        }

        public PicturePickupEvent(Side source, Picture picture, int row,
                int col) {
            super(source, picture, new Index(row, col));
        }
    }
}
